package cn.caplike.demo.spring.security.csrf.configuration.security;

import cn.caplike.data.redis.service.spring.boot.starter.RedisKey;
import cn.caplike.data.redis.service.spring.boot.starter.RedisService;
import cn.caplike.demo.spring.security.csrf.domain.dto.LoginUser;
import com.alibaba.fastjson.JSON;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;

/**
 * 基于 Redis 的 CSRF Token Repository
 * <pre>
 *  ENDPOINT: /login
 * 	FIRST-CALL
 * 		|  INFO 3908 --- [o-18903-exec-10] .c.f.RequestBodyServletRequestWrapFilter : request body servlet request wrap filter ...
 * 		| DEBUG 3908 --- [o-18903-exec-10] c.c.d.s.s.c.c.s.RedisCsrfTokenRepository : csrf filter: redis csrf token repository: load token by LoginUser info (LoginUser(username=caplike, password=caplike)).
 * 		| DEBUG 3908 --- [o-18903-exec-10] c.c.d.s.s.c.c.s.RedisCsrfTokenRepository : csrf filter: redis csrf token repository: generate token: 53aa5d4b2f7a4574be867dad71a63af3
 * 		| DEBUG 3908 --- [o-18903-exec-10] c.c.d.s.s.c.c.s.RedisCsrfTokenRepository : csrf filter: redis csrf token repository: save token
 * 		|  INFO 3908 --- [o-18903-exec-10] c.c.d.s.s.c.f.SimpleAuthenticationFilter : simple authentication filter: attemptAuthentication ...
 *
 * 	SECOND-CALL
 * 		|  INFO 19260 --- [io-18903-exec-3] .c.f.RequestBodyServletRequestWrapFilter : request body servlet request wrap filter ...
 * 		|  ---> call CsrfFilter 的 doInternalFilter
 * 		| DEBUG 19260 --- [io-18903-exec-3] c.c.d.s.s.c.c.s.RedisCsrfTokenRepository : csrf filter: redis csrf token repository: load token by LoginUser info (LoginUser(username=caplike, password=caplike)).
 * 		|  INFO 19260 --- [io-18903-exec-3] c.c.d.s.s.c.f.SimpleAuthenticationFilter : simple authentication filter: attemptAuthentication ...
 * </pre>
 *
 * @author LiKe
 * @version 1.0.0
 * @date 2020-04-26 16:59
 */
@Slf4j
@Component
public class CsrfTokenRedisRepository implements CsrfTokenRepository {

    /**
     * parameterName
     */
    private static final String CSRF_PARAMETER_NAME = "_csrf";

    /**
     * headerName
     */
    private static final String CSRF_HEADER_NAME = "X-CSRF-TOKEN";

    private RedisService redisService;

    private LoginUser loginUser;

    /**
     * 该方法会被调用次数: <br>
     * 1. 第一次是在 CsrfFilter 中, 当 loadToken 的调用返回 null 时;<br>
     * 2. (非匿名用户) 第二次是在 {@link CsrfAuthenticationStrategy#onAuthentication(Authentication, HttpServletRequest, HttpServletResponse)} 用于执行清除, 此时传入的参数 token 为 null;
     * 3. (非匿名用户) 第三次实在 {@link CsrfAuthenticationStrategy#onAuthentication(Authentication, HttpServletRequest, HttpServletResponse)} 用于更新;
     */
    @SneakyThrows
    @Override
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
        log.debug("csrf filter: redis csrf token repository: save token");
        // request.getSession().setAttribute(CSRF_TOKEN_ATTR_NAME, token);

        // 由于过期时间交由 Redis 管理, 所以 token 为 null 时, 不进行任何操作直接返回.
        if (Objects.isNull(token)) {
            log.debug("csrf filter: do nothing while token is null. The token's lifecycle will be handled by Redis.");
            return;
        }

        redisService.setValue(
                RedisKey.builder()
                        .prefix("user")
                        .suffix(Optional.ofNullable(loginUser).orElseThrow(() -> new AuthenticationException("LoginUser info is null!") {
                        }).getUsername())
                        .build(),
                token.getToken()
        );

        response.setHeader("csrf-token", token.getToken());
    }

    @SneakyThrows
    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        // HttpSession httpSession = Objects.requireNonNull(request.getSession(false), "当前无 Session!");
        // return (CsrfToken) httpSession.getAttribute(CSRF_TOKEN_ATTR_NAME);

        final LoginUser loginUser = setLoginUser(request);
        log.debug("csrf filter: redis csrf token repository: load token by LoginUser info ({}).", loginUser.toString());

        try {
            final String csrfToken = getCachedToken();

            return StringUtils.isBlank(csrfToken) ? null : new DefaultCsrfToken(
                    CSRF_HEADER_NAME,
                    CSRF_PARAMETER_NAME,
                    csrfToken
            );
        } catch (RuntimeException ignored) {
            return null;
        }
    }

    private String getCachedToken() {
        final String csrfToken = redisService.getValue(
                // demo-spring-security-csrf.user.{username}
                RedisKey.builder().prefix("user").suffix(loginUser.getUsername()).build(),
                String.class
        );
        if (StringUtils.isNoneBlank(csrfToken)) {
            return csrfToken;
        }

        return StringUtils.EMPTY;
    }

    private LoginUser setLoginUser(HttpServletRequest request) throws java.io.IOException {
        final LoginUser loginUser = (LoginUser) Optional.ofNullable(JSON.parseObject(request.getInputStream(), StandardCharsets.UTF_8, LoginUser.class))
                .orElseThrow(() -> new AuthenticationException("LoginUser info is null!") {
                });
        this.loginUser = loginUser;
        return loginUser;
    }

    @Override
    public CsrfToken generateToken(HttpServletRequest request) {
        final String csrfToken = StringUtils.replace(UUID.randomUUID().toString(), "-", StringUtils.EMPTY);
        log.debug("csrf filter: redis csrf token repository: generate token: {}", csrfToken);

        return new DefaultCsrfToken(CSRF_HEADER_NAME, CSRF_PARAMETER_NAME, csrfToken);
    }

    // ~ Autowired
    // -----------------------------------------------------------------------------------------------------------------

    @Autowired
    public void setRedisService(RedisService redisService) {
        this.redisService = redisService;
    }
}
